package com.fzk.server;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.fzk.exception.IllegalParamsException;
import com.fzk.log.Logger;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import io.netty.util.AsciiString;
import org.apache.commons.lang3.StringUtils;

import javax.activation.MimetypesFileTypeMap;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;

/**
 * @author fzk
 * @datetime 2023-02-13 23:46:10
 */
public class MyHttpUtil {
    // 图片类型后缀名
    public static final Set<String> Image_Suffix = Set.of("png", "jpeg", "jpg",// sb windows的后缀
            "gif", "bmp", "webp", "x-icon", "ico", "tiff", "svg");
    // 浏览器可以直接显示的内容类型
    public static final Set<String> Inline_ContentType = Set.of("text/plain", "text/html", "text/css", "text/javascript",

            "image/jpeg", "image/png", "image/gif", "image/svg+xml",// 矢量图
            "image/webp", "image/x-icon",

            "audio/mpeg", "audio/ogg", "audio/*", "video/mp4,", "application/json", "application/javascript", "application/ecmascript");
    // 浏览器作为下载的内容类型
    public static final Set<String> Attachment_ContentType = Set.of("application/octet-stream"// 未知文件类型
    );

    /**
     * 获取文件对应的content-type
     *
     * @param path 文件路径
     */
    public static String getContentType(Path path) {
        MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
        // javax.activation包下META-INF/mimetypes.default里并不是很全，需要手动添加一些
        mimeTypesMap.addMimeTypes("type=text/css exts=css");// 注意：css文件必须以text/css响应，否则浏览器不会加载此css文件进行页面渲染
        mimeTypesMap.addMimeTypes("type=text/javascript exts=js");
        mimeTypesMap.addMimeTypes("type=image/x-icon exts=ico");// 图标类型居然都没有
        mimeTypesMap.addMimeTypes("type=image/bmp exts=bmp");
        mimeTypesMap.addMimeTypes("type=image/webp exts=webp");
        mimeTypesMap.addMimeTypes("type=application/pdf exts=pdf");
        return mimeTypesMap.getContentType(path.toFile());
    }

    /**
     * HTTP场景中, content-disposition表示浏览器如何处理文件
     * Content-Disposition: inline. 默认, 表示浏览器将展示内容
     * Content-Disposition: attachment; filename="filename.jpg", 表示下载文件到本地并指定文件名
     *
     * @param path 文件地址
     * @return 是否应该下载到本地, 应尽可能直接展示
     */
    public static boolean isAttachment(Path path) {
        // 1.先判断是否为inline type
        String contentType = getContentType(path);
        if (Inline_ContentType.contains(contentType)) {
            return false;
        }
        // 2.判断是否为图片后缀
        String suffix = getSuffix(path);
        if (Image_Suffix.contains(suffix)) {
            return false;
        }

        // 3.最后再判断是否为直接下载类型
        if (Attachment_ContentType.contains(contentType)) {
            return true;
        }
        return false;// 尽可能不使用Content-Disposition: attachment; filename="xxx"
    }

    public static String getSuffix(Path path) {
        String filename = path.getFileName().toString();
        String[] splits = filename.split("\\.");
        if (splits.length <= 1) {// 没有后缀
            return "";
        }
        return splits[splits.length - 1];
    }

    /**
     * 默认将utf-8字符集添加到content-type
     */
    public static void addCharsetToContentType(DefaultHttpResponse response) {
        String contentType = response.headers().get(HttpHeaderNames.CONTENT_TYPE);
        if (contentType == null || contentType.isEmpty())
            throw new RuntimeException("向响应头添加charset=utf-8之前, 必须提前设置content-type响应头");
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType + ";charset=utf-8");
    }

    // Content-Type: application/x-www-form-urlencoded
    public static Map<String, List<String>> parseFormParams(FullHttpRequest request) throws IllegalParamsException {
        if (!isContentType(request, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED)) {
            return new HashMap<>();
        }
        // key=hello&k1=1&k1=2&k2=100
        String bodyStr = new String(ByteBufUtil.getBytes(request.content()), StandardCharsets.UTF_8);
        if (StringUtils.isEmpty(bodyStr)) {// 空请求体无参数
            return Collections.emptyMap();
        }
        String[] splits = bodyStr.split("&");
        Map<String, List<String>> params = new HashMap<>(splits.length);
        for (String split : splits) {
            String[] kv = split.split("=");
            if (kv.length != 2) {
                Logger.warning(String.format("invalid params body format, body: %s", bodyStr));
                throw new IllegalParamsException("参数错误");
            }
            // key和value都必须先url解码
            String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8);
            String value = URLDecoder.decode(kv[1], StandardCharsets.UTF_8);
            List<String> list = params.computeIfAbsent(key, k -> new ArrayList<>(1));
            list.add(value);
        }
        return params;
    }

    // Content-Type: application/json
    public static JSONObject parseJsonParams(FullHttpRequest request) throws IllegalParamsException {
        if (!isContentType(request, HttpHeaderValues.APPLICATION_JSON)) {
            return new JSONObject();
        }
        String bodyStr = new String(ByteBufUtil.getBytes(request.content()), StandardCharsets.UTF_8);
        try {
            return JSON.parseObject(bodyStr);
        } catch (Exception e) {
            Logger.error(String.format("解析json格式请求体参数出错, err: %s，req: %s\n\n%s", e, request, bodyStr));
            throw new IllegalParamsException("解析json格式请求体参数出错，可能是参数格式有误", e);
        }
    }

    // Content-Type: application/json
    public static <T> T parseJsonParams(FullHttpRequest request, Class<T> t) throws IllegalParamsException {
        if (!isContentType(request, HttpHeaderValues.APPLICATION_JSON)) {
            return null;
        }
        String bodyStr = new String(ByteBufUtil.getBytes(request.content()), StandardCharsets.UTF_8);
        try {
            return JSON.parseObject(bodyStr, t);
        } catch (Exception e) {
            Logger.error(String.format("解析json格式请求体参数出错, err: %s，req: %s\n\n%s", e, request, bodyStr));
            throw new IllegalParamsException("解析json格式请求体参数出错，可能是参数格式有误", e);
        }
    }

    public static boolean isContentType(HttpRequest request, AsciiString contentType) {
        return contentType.contentEqualsIgnoreCase(request.headers().get(HttpHeaderNames.CONTENT_TYPE));
    }

    /**
     * 在使用 Nginx 作为反向代理时，获取到的连接地址是nginx服务器地址。nginx需配置X-Real-IP和X-Forwarded-For请求头传递客户端ip供后端获取。
     * X-Real-IP：客户端ip
     * X-Forwarded-For: 代理链ip列表：客户端IP, 代理1IP, 代理2IP, ...
     * 如果请求经过多层代理（如 CDN + Nginx），X-Forwarded-For 会包含多个 IP 地址
     * 注意：这个请求头需防止客户端伪造
     *
     * @return 客户端地址
     */
    public static String getClientAddress(ChannelHandlerContext ctx, FullHttpRequest request) {
        SocketAddress sa = ctx.channel().remoteAddress();
        if (sa instanceof InetSocketAddress) {
            InetAddress ia = ((InetSocketAddress) sa).getAddress();
            if (// refer to RFC 1918
                // 10/8 prefix
                // 172.16/12 prefix
                // 192.168/16 prefix
                    !(ia.isSiteLocalAddress() || ia.isLoopbackAddress()// 127.x.x.x
                            || ia.isAnyLocalAddress()// 0.0.0.0
                            || ia.isLinkLocalAddress())// 169.254.0.0/16
            ) {// 1.非本地 客户端ip，可直接返回，不判断 X-Forwarded-For 避免客户端伪造
                return ia.getHostAddress();
            }
            // 2.本地客户端，应该是nginx进行了转发
            // 判断这个请求头有伪造风险哎
            String xForwardedFor = request.headers().get(HttpHeaderNameXForwardedFor);
            if (StringUtils.isNotEmpty(xForwardedFor)) {
                String[] split = xForwardedFor.split(",\\s+");
                if (split.length >= 1) return split[0];
            }
            // 3.本地客户端且没有转发
            return ia.getHostAddress();
        }
        return sa.toString();// hostname/hostAddress:port, hostname often null
    }

    public static final String HttpHeaderNameXForwardedFor = "X-Forwarded-For";
    public static final String HttpHeaderNameXRealIP = "X-Real-IP";
}